IconPicker 图标选择组件(上)
组件需求
IconPicker 是一个常见的业务组件,核心交互流程:
- 点击按钮弹出 Dialog
- Dialog 内展示图标网格列表
- 可设置颜色(ColorPicker)和字号(InputNumber)
- 点击选中图标,高亮显示
- 确认后 emit 选中的图标信息,取消则关闭
类型定义
// src/components/Icons/types.ts
export interface IconPickerSubmitData {
/** 选中的图标名,格式: collection:icon-name */
icon: string
/** 图标颜色 */
color: string
/** 图标字号大小 */
fontSize: number
}
export interface IconPickerProps {
/** Dialog 宽度 */
width?: string
/** 按钮文字 */
buttonText?: string
}
export interface IconPickerEmits {
(e: 'submit', data: IconPickerSubmitData): void
(e: 'cancel'): void
}
typescript
IconList 组件增强
在 IconList 组件中增加选中状态支持:
<!-- src/components/Icons/IconList.vue -->
<script setup lang="ts">
import { ref, onBeforeMount } from 'vue'
import { Icon, loadIcons } from '@iconify/vue'
import type { IconListProps } from './types'
import iconData from '@/assets/icons/icon-ep.json'
const props = withDefaults(defineProps<IconListProps & {
/** 选中项的高亮 class */
activeClass?: string
}>(), {
collection: 'ep',
iconData: () => iconData,
showText: false,
iconClass: 'text-2xl',
itemClass: '',
activeClass: '',
})
const emit = defineEmits<{
click: [name: string, index: number]
}>()
// 当前选中索引
const chosen = ref(-1)
onBeforeMount(async () => {
if (props.iconData?.length) {
const icons = props.iconData.map(
(name) => `${props.collection}:${name}`
)
await loadIcons(icons)
}
})
function handleClick(name: string, index: number) {
chosen.value = index
emit('click', `${props.collection}:${name}`, index)
}
</script>
<template>
<ul
class="grid border-l border-t border-gray-200"
:style="{
gridTemplateColumns: 'repeat(auto-fill, minmax(1.825rem, 1fr))',
}"
>
<li
v-for="(name, index) in iconData"
:key="index"
class="flex items-center justify-center border-r border-b border-gray-200
cursor-pointer hover:bg-sky-50 transition-colors duration-200 py-2"
:class="[
itemClass,
chosen === index ? activeClass : '',
]"
@click="handleClick(name, index)"
>
<Icon
:icon="`${collection}:${name}`"
:class="iconClass"
/>
<span
v-if="showText"
class="text-xs text-gray-500 mt-1 truncate w-full text-center px-1"
>
{{ name }}
</span>
</li>
</ul>
</template>
vue
布局优化:Flex 改 Grid
使用 CSS Grid 替代 Flex 布局解决末尾空白问题:
/* Flex 布局问题:固定宽度的 item 换行后最后一行可能不填满 */
/* Grid 方案:auto-fill + minmax 自适应列数 */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(1.825rem, 1fr));
border-left: 1px solid #e5e7eb;
border-top: 1px solid #e5e7eb;
}
/* 每个 item 只加右边和底部的 border,避免双线 */
.icon-grid > li {
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
}
css
minmax(1.825rem, 1fr) 的含义:
- 每列最小宽度为
1.825rem(约等于text-2xl的图标大小) - 最大为
1fr,自动平分剩余空间 - 列数随容器宽度自动调整
IconPicker 组件实现
<!-- src/components/Icons/IconPicker.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { Icon } from '@iconify/vue'
import { useToggle } from '@vueuse/core'
import {
ElButton,
ElDialog,
ElColorPicker,
ElInputNumber,
} from 'element-plus'
import IconList from './IconList.vue'
import type { IconPickerSubmitData } from './types'
interface IconPickerProps {
width?: string
buttonText?: string
}
const props = withDefaults(defineProps<IconPickerProps>(), {
width: '50%',
buttonText: '选择图标',
})
const emit = defineEmits<{
submit: [data: IconPickerSubmitData]
cancel: []
}>()
// Dialog 显隐
const [show, toggle] = useToggle(false)
// 选中状态
const iconRef = ref('')
const color = ref('#409EFF')
const fontSize = ref(16)
// 图标选中回调
function handleClick(icon: string) {
iconRef.value = icon
}
// 确认
function handleConfirm() {
toggle(false)
emit('submit', {
icon: iconRef.value,
color: color.value,
fontSize: fontSize.value,
})
}
// 取消
function handleCancel() {
toggle(false)
emit('cancel')
}
</script>
<template>
<div>
<!-- 触发按钮 -->
<ElButton @click="toggle(true)">
<slot>{{ buttonText }}</slot>
</ElButton>
<!-- 弹窗 -->
<ElDialog v-model="show" title="选择图标" :width="width">
<!-- 设置区 -->
<div class="flex items-center gap-4 py-2">
<ElColorPicker v-model="color" size="small" />
<ElInputNumber
v-model="fontSize"
:min="12"
:max="64"
:step="1"
size="small"
/>
</div>
<!-- 预览区 + 图标列表 -->
<div class="flex gap-4">
<!-- 左侧预览 -->
<div class="flex-shrink-0 w-20 flex items-center justify-center border rounded">
<Icon
v-if="iconRef"
:icon="iconRef"
:style="{ color: color, fontSize: fontSize + 'px' }"
/>
<span v-else class="text-xs text-gray-400">请选择</span>
</div>
<!-- 右侧图标列表 -->
<div class="flex-1 max-h-[400px] overflow-y-auto">
<IconList
:show-text="false"
icon-class="text-xl"
:active-class="'text-sky-500'"
@click="handleClick"
/>
</div>
</div>
<!-- 底部操作 -->
<template #footer>
<ElButton @click="handleCancel">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确定</elButton>
</template>
</ElDialog>
</div>
</template>
vue
业务页面
<!-- src/pages/components/icons/ep-iconpicker.vue -->
<script setup lang="ts">
import IconPicker from '@/components/Icons/IconPicker.vue'
import type { IconPickerSubmitData } from '@/components/Icons/types'
function handleSubmit(data: IconPickerSubmitData) {
console.log('选中的图标:', data)
// data = { icon: 'ep:home', color: '#409EFF', fontSize: 16 }
}
</script>
<template>
<div class="p-6">
<h2 class="text-2xl font-bold mb-6">IconPicker 图标选择器</h2>
<IconPicker @submit="handleSubmit" />
</div>
</template>
vue
useToggle 工具函数
来自 VueUse 的 useToggle,提供布尔值的切换能力:
import { useToggle } from '@vueuse/core'
const [show, toggle] = useToggle(false)
show.value // false
toggle() // show 变为 true
toggle(true) // show 变为 true
toggle(false) // show 变为 false
typescript
相比手动管理 ref:
// 手动写法
const show = ref(false)
const open = () => (show.value = true)
const close = () => (show.value = false)
// useToggle 写法(更简洁)
const [show, toggle] = useToggle(false)
typescript
关键实现细节
选中状态传递
IconList 的 click 事件需要传递完整的图标名(含 collection 前缀):
// IconList 组件中
function handleClick(name: string, index: number) {
chosen.value = index
// 传递完整的 'ep:home' 而非仅 'home'
emit('click', `${props.collection}:${name}`, index)
}
typescript
动态 activeClass
选中高亮通过动态 class 绑定实现:
<li
:class="[
itemClass,
chosen === index ? activeClass : '',
]"
>
vue
父组件传入 activeClass 控制高亮样式:
<IconList active-class="text-sky-500" @click="handleClick" />
vue
预览区实时更新
通过 v-model 绑定的 color 和 fontSize 实时响应:
<Icon
:icon="iconRef"
:style="{
color: color,
fontSize: fontSize + 'px',
}"
/>
vue
组件通信流程
IconPicker(父)
├── ElButton(触发弹窗)
├── ElDialog(弹窗容器)
│ ├── ElColorPicker ──→ color ref
│ ├── ElInputNumber ──→ fontSize ref
│ ├── 预览区(Icon + style 绑定)
│ └── IconList(子组件)
│ └── @click ──→ iconRef 更新
└── Footer(确定/取消)
├── 确定 ──→ emit('submit', { icon, color, fontSize })
└── 取消 ──→ emit('cancel')
text
↑